package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private final StringRedisTemplate stringRedisTemplate;
    private final CacheClient cacheClient;

    /**
     * 1、缓存穿透：客户端请求的数据在缓存和数据库中都不存在，这样缓存永远不会生效，这些请求都会打到数据库<br/>
     * 解决方案：(1)缓存空对象<br/>
     *         (2)布隆过滤<br/>
     * 2、缓存雪崩是指在通义时段大量缓存key同时失效或redis服务宕机，导致大量请求到达数据库，带来巨大压力<br/>
     * 解决方案：(1)给不同的key的TTL添加随机值<br/>
     *         (2)利用redis集群提高服务的可用性<br/>
     *         (3)给缓存业务添加降级限流策略<br/>
     *         (4)给业务添加多级缓存<br/>
     * 3、缓存击穿问题也叫热点key问题，就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了，无数请求的访问会瞬间给数据库带来强大冲击。<br/>
     * 解决方案：(1)互斥锁：某一线程查询缓存未命中，则尝试获取互斥锁，获取锁成功，则再次查询缓存，命中则返回，未命中则进行缓存重建，最后释放锁并返回；
     *             获取锁失败，则等待一段时间后重新查询redis缓存、尝试获取锁...<br/>
     *             (优点: 没有额外的内存消耗、保证一致性、实现简单;<br/>
     *             缺点: 缓存重建业务较复杂，且重建时其他线程只能等待，影响性能、可能有死锁的风险)<br/>
     *         (2)逻辑过期：提前写入缓存并设置逻辑过期时间，不设置 TTL，某一线程查询缓存命中但发现已过期，则尝试获取互斥锁，获取锁成功，则再次查询缓存，命中且未过期则返回，
     *            过期则(利用线程池)开启一段新线程进行缓存重建，后释放锁，原来的线程则直接返回过期数据；
     *            获取锁失败，则直接返回过期数据。<br/>
     *            (优点: 线程无需等待，性能好;<br/>
     *            缺点: 不保证一致性、有额外内存消耗、实现复杂)<br/>
     */
    @Override
    public Result queryById(Long id) {
        // 查询 1、 —— 解决缓存穿透
//        Shop shop = queryWithPassThrough(id);
        // 1.1、将方法封装后调用 (调用sql方法时，Id -> getById(Id) 可简写为 this::getById )
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 查询 2、 —— 使用互斥锁解决缓存穿透+缓存击穿
//        Shop shop = queryWithMutex(id);
        Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY, LOCK_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, CACHE_NULL_TTL, TimeUnit.MINUTES);

        // 查询 3、 —— 使用逻辑过期解决缓存穿透+缓存击穿
//        Shop shop = queryWithLogicalExpire(id);
//        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, LOCK_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        if(shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);


    }

    public Shop queryWithPassThrough(Long id){
        //缓存穿透解决方案(防止通过不断查询不存在的值恶意攻击数据库)

        String key = CACHE_SHOP_KEY+id;
        //1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3、存在，直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断是否为空值（解决缓存穿透————不断查询不存在的值）
        if(shopJson != null){
            return  null;
        }

        //4、不存在，根据id查询数据库
        Shop shop = getById(id);
        //5、不存在，返回空
        if(shop == null){
            //向redis中写入空值（解决缓存穿透————不断查询不存在的值）
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);

            return null;
        }
        //6、存在，写入redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7、返回
        return shop;
    }

    public Shop queryWithMutex(Long id){
        //使用互斥锁解决缓存击穿(防止在高并发的情况下查询数据库导致的数据库崩溃)

        String key = CACHE_SHOP_KEY+id;
        String LockKey = "lock:shop:"+id;
        Shop shop = null;
        //1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3、存在，直接返回
            shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //4、判断是否为空值（解决缓存穿透————不断查询不存在的值）
        if(shopJson != null){
            return  null;
        }

        try {
            //5、未命中，实现缓存重建
            //5.1、尝试获取互斥锁
            boolean isLock = tryLock(LockKey);
            if(!isLock){
                //5.2、获取锁失败，休眠后再次尝试获取互斥锁
                Thread.sleep(50);
                queryWithMutex(id);
            }
            //5.3、获取互斥锁成功，再次检测redis缓存是否存在
            shopJson = stringRedisTemplate.opsForValue().get(key);
            if(StrUtil.isNotBlank(shopJson)){
                unLock(LockKey);
                return JSONUtil.toBean(shopJson, Shop.class);
            }

            //6、不存在，根据id查询数据库
            shop = getById(id);
            // TODO 模拟重建延时
            Thread.sleep(200);
            //7、不存在，返回空
            if(shop == null){
                //向redis中写入空值（解决缓存穿透————不断查询不存在的值）
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6、存在，写入redis
            String jsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        }
        catch (Exception e){
            throw new RuntimeException(e);
        }
        finally {
            //7、释放互斥锁
            unLock(LockKey);
        }
        //8、返回
        return shop;
    }

    //创建一个十个线程的线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public Shop queryWithLogicalExpire(Long id){
        //使用逻辑过期解决缓存击穿(防止在高并发的情况下查询数据库导致的数据库崩溃)

        String key = CACHE_SHOP_KEY+id;
        String LockKey = "lock:shop:"+id;
        //1、从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isBlank(shopJson)){
            //3、不存在，直接返回null
            return null;
        }
        //4、命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        //5、判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDateTime.now())){
            //5.1、未过期，直接返回对象
            return shop;
        }
        //6、过期，需要缓存重建
        //6.1、获取互斥锁
        boolean isLock = tryLock(LockKey);
        if(isLock){
            //6.2、获取互斥锁成功，开启独立线程，实现缓存重建
            //6.3、再次检测redis缓存是否过期
            shopJson = stringRedisTemplate.opsForValue().get(key);
            //判断是否存在
            if(StrUtil.isBlank(shopJson)){
                //不存在，直接返回null
                return null;
            }
            //命中，需要先把json反序列化为对象
            redisData = JSONUtil.toBean(shopJson, RedisData.class);
            data = (JSONObject)redisData.getData();
            shop = JSONUtil.toBean(data, Shop.class);
            //判断是否过期
            expireTime = redisData.getExpireTime();
            if(expireTime.isAfter(LocalDateTime.now())){
                //未过期，直接返回对象
                unLock(LockKey);
                return shop;
            }
            // 过期，重建缓存
            // 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    saveShopToRedis(id, 20L);
                }
                catch (Exception e){
                    throw new RuntimeException(e);
                }
                finally {
                    //释放锁
                    unLock(LockKey);
                }
            });
        }
        //6.3、返回过期的商铺信息
        return shop;
    }

    /**
     * 尝试获取互斥锁
     * @param key
     * @return
     */
    public boolean tryLock(String key){
        Boolean rock = stringRedisTemplate.opsForValue().setIfAbsent(key, "rock", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(rock);
    }

    /**
     * 释放互斥锁
     * @param key
     */
    public void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
        // 1、查询店铺数据
        Shop shop = getById(id);
        //模拟重建延时
        Thread.sleep(200);
        // 2、封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3、写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 1.判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 不需要坐标查询，按数据库查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));//分页查询
            // 返回数据
            return Result.ok(page.getRecords());
        }

        // 2.计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 3.查询redis、按照距离排序、分页。结果：shopId、distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y), //使用经纬度关联的圆心
                        new Distance(5000), //半径值(默认单位为米)
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                                .includeDistance()//添加搜索参数
                                .limit(end) //指定分页参数
                );
        // 4.解析出id
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了，结束
            return Result.ok(Collections.emptyList());
        }
        // 4.1.截取 from ~ end的部分
        List<Long> ids = new ArrayList<>(list.size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            // 4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5.根据id查询Shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6.返回
        return Result.ok(shops);
    }

    @Override
    public Result updateShop(Shop shop) {
        //检查是否有id
        Long id = shop.getId();
        if(id == null){
            return Result.fail("用户id不能为空");
        }
        //更新数据库
        updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
        return Result.ok();
    }
}
